package top.v5it.japi.plus.core.spring.web;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerMapping;
import top.v5it.japi.plus.common.util.JackSonUtil;
import top.v5it.japi.plus.core.ConstantPool;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.stream.Stream;

/**
 * 解密HttpServletRequest中的数据
 *
 * @author zhanpu
 */
@Slf4j
public class JapiDecryptHttpServletRequestWrapper extends JapiHttpServletRequestWrapper {

    /**
     * 构造函数
     *
     * @param request {@link HttpServletRequest}
     */
    public JapiDecryptHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request, Boolean.FALSE);
    }

    /**
     * 获取{@link JapiEada}
     *
     * @return {@link JapiEada}
     */
    public JapiEada getJapiEada() {
        return (JapiEada) Optional
                .ofNullable(super.getAttribute(ConstantPool.JAPIEADA_NAME))
                .orElse(null);
    }

    /**
     * 获取算法
     *
     * @return 算法 {@link EadaAlgorithm}
     */
    public EadaAlgorithm getAlgorithm() {
        return (EadaAlgorithm) Optional
                .ofNullable(super.getAttribute(ConstantPool.JAPIEADA_ALGORITHM_NAME))
                .orElse(null);
    }

    @Override
    String clean(String value) {
        return orgRequest.getClass() == JapiXssHttpServletRequestWrapper.class || this.getClass().isAssignableFrom(JapiXssHttpServletRequestWrapper.class)
                ? super.clean(value)
                : value;
    }

    /**
     * 覆盖getParameter方法，将参数名和参数值都做xss过滤。<br/>
     * 如果需要获得原始的值，则通过super.getParameterValues(name)来获取<br/>
     * getParameterNames,getParameterValues和getParameterMap也可能需要覆盖
     */
    @Override
    public String getParameter(String name) {
        name = clean(name);
        // 获取源数据
        final String value = super.getParameter(name);

        // 为空直接返回
        if (StrUtil.isBlank(value)) {
            return value;
        }

        // 非resource字段，直接返回
        if (!StrUtil.equals(ConstantPool.RESOURCE, name)) {
            return clean(value);
        }

        final String resource = URLUtil.encodeQuery(value);

        // 1、检查请求是否需要解密；需要进行下步
        // 2、检查请求参数resource_type是否密文，否则按服务器设置
        // 3、解密
        String newValue = Optional.ofNullable(getJapiEada())
                .filter(JapiEada::isDecrypt)
                .flatMap(v -> {
                    // 获取resource_type
                    JapiEada.Type type =
                            Optional.ofNullable(super.getParameter(ConstantPool.RESOURCE_TYPE))
                                    .map(JapiEada.Type::match)
                                    .orElse(v.type());

                    // resource_type不为空并且值为ENCRYPT或为空则解密
                    return JapiEadaUtil
                            .resourceApply(type, getAlgorithm().getEadaFunc(false))
                            .apply(resource);
                })
                .orElse(resource);

        return clean(newValue);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> parameterMap = super.getParameterMap();

        if (!parameterMap.containsKey(ConstantPool.RESOURCE)) {
            return clean(parameterMap);
        }

        parameterMap.computeIfPresent(ConstantPool.RESOURCE, (key, val) -> new String[]{this.getParameter(key)});
        return clean(parameterMap);
    }

    private Map<String, String[]> clean(Map<String, String[]> param) {
        Map<String, String[]> rtvMap = new HashMap<>(param.size());
        param.forEach((k, v) ->
                rtvMap.put(clean(k)
                        , Stream.of(v)
                                .map(this::clean)
                                .toArray(String[]::new)));

        return rtvMap;
    }

    /**
     * 覆盖getParameterValues方法
     *
     * @param name
     * @return
     */
    @Override
    public String[] getParameterValues(String name) {
        String[] parameterValues = super.getParameterValues(clean(name));
        if (ArrayUtil.isEmpty(parameterValues)) return null;
        return Stream
                .of(parameterValues)
                .map(this::clean)
                .toArray(String[]::new);
    }

    /**
     * 覆盖getHeader方法，将参数名和参数值都做xss过滤。<br/>
     * 如果需要获得原始的值，则通过super.getHeaders(name)来获取<br/>
     * getHeaderNames 也可能需要覆盖
     */
    @Override
    public String getHeader(String name) {
        return clean(super.getHeader(clean(name)));
    }

    @Override
    public String getQueryString() {
        String queryString = super.getQueryString();

        if (StrUtil.isBlank(queryString)) {
            return queryString;
        }

        // JapiEada为null，则直接返回
        Optional<JapiEada> japiEadaOptional =
                Optional.ofNullable(getJapiEada())
                        .filter(JapiEada::isDecrypt);

        if (!japiEadaOptional.isPresent()) {
            return clean(queryString);
        }

        Map<String, String> queryMap = HttpUtil.decodeParamMap(queryString, Charset.defaultCharset());

        if (!queryMap.containsKey(ConstantPool.RESOURCE)) {
            return clean(queryString);
        }

        String resource = queryMap.get(ConstantPool.RESOURCE);

        if (StrUtil.isBlank(resource)) {
            return clean(queryString);
        }

        // 获取resource_type
        JapiEada.Type type =
                Optional.ofNullable(queryMap.get(ConstantPool.RESOURCE_TYPE))
                        .map(JapiEada.Type::match)
                        .orElse(japiEadaOptional.get().type());

        JapiEadaUtil
                .resourceApply(type, getAlgorithm().getEadaFunc(false))
                .apply(resource)
                .ifPresent(v -> queryMap.replace(ConstantPool.RESOURCE, resource, v));

        return clean(HttpUtil.toParams(queryMap));
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object getAttribute(String name) {
        name = clean(name);

        Optional<JapiEada> japiEadaOptional =
                Optional.ofNullable(getJapiEada())
                        .filter(JapiEada::isDecrypt);

        if (!japiEadaOptional.isPresent()) {
            return super.getAttribute(name);
        }

        if (HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE.equals(name)) {
            Object attribute = super.getAttribute(name);
            LinkedHashMap linkedHashMap = (LinkedHashMap) attribute;
            if (linkedHashMap.containsKey(ConstantPool.RESOURCE)) {
                Object resource = linkedHashMap.get(ConstantPool.RESOURCE);

                // 获取resource_type
                JapiEada.Type type =
                        Optional.ofNullable(linkedHashMap.get(ConstantPool.RESOURCE_TYPE))
                                .map(Objects::toString)
                                .map(JapiEada.Type::match)
                                .orElse(japiEadaOptional.get().type());

                LinkedHashMap rtv = new LinkedHashMap(linkedHashMap);
                JapiEadaUtil
                        .resourceApply(type, getAlgorithm().getEadaFunc(false))
                        .apply(Objects.toString(resource))
                        .ifPresent(v -> rtv.replace(ConstantPool.RESOURCE, resource, v));
                return rtv;
            }
        }

        return super.getAttribute(name);
    }

    /**
     * 重写getInputStream
     *
     * @return
     * @throws IOException
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {

        String bodyOld = getBody();

        if (StrUtil.isBlank(bodyOld)) {
            return super.getInputStream();
        }

        // 非json类型，直接返回
        if (isAcceptContentTypeRequest()) {
            return new JapiServletInputStream(new ByteArrayInputStream(clean(bodyOld).getBytes()));
        }

        // 获取需要解密的字段，如果为空，直接返回
        JsonNode jsonNode;

        try {
            jsonNode = JackSonUtil.getMapper().readTree(bodyOld);
        } catch (JsonProcessingException e) {
            log.error("请求参数格式错误\n{}", bodyOld);
            throw new IllegalArgumentException("请求参数格式错误");
        }

        String bodyNew =
                Optional.ofNullable(getJapiEada())
                        .filter(JapiEada::isDecrypt)
                        .map(v -> JapiEadaUtil.findResourceApply(jsonNode, getAlgorithm().getEadaMapper(false, v)))
                        .map(JsonNode::toString)
                        .orElse(bodyOld);

        return new JapiServletInputStream(new ByteArrayInputStream(clean(bodyNew).getBytes()));
    }

}
